package com.tao.lab.power.usb;

import android.util.Log;

import com.tao.lab.power.usb.model.UsbSerialStatus;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * title:
 * description:
 * @author gongmin.li
 * @version 1.0.0
 * @since 2020-06-15
 */
public class UsbSerialIOManager implements Runnable {

    private static final String TAG = UsbSerialIOManager.class.getSimpleName();

    private static final int DEFAULT_BUFFER_SIZE = 4096;

    private final ByteBuffer mReadBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
    private final ByteBuffer mWriteBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); // Synchronized by 'mWriteBuffer'

    /**
     * default read timeout is infinite, to avoid data loss with bulkTransfer API
     */
    private int mReadTimeout = 0;
    private int mWriteTimeout = 0;

    private final UsbSerialPort mSerialPort;
    private UsbSerialIOListener mUsbSerialIOListener;
    private UsbSerialStatus mUsbSerialStatus;

    public UsbSerialIOManager(UsbSerialPort serialPort,
                              UsbSerialIOListener usbSerialIOListener, UsbSerialStatus usbSerialStatus) {
        this.mSerialPort = serialPort;
        this.mUsbSerialIOListener = usbSerialIOListener;
        setSensorStatus(usbSerialStatus);
    }

    private synchronized UsbSerialIOListener getSerialIOListener() {
        return mUsbSerialIOListener;
    }

    public synchronized UsbSerialStatus getSensorStatus() {
        return mUsbSerialStatus;
    }

    public synchronized void setSensorStatus(UsbSerialStatus mUsbSerialStatus) {
        this.mUsbSerialStatus = mUsbSerialStatus;
    }

    public void setReadTimeout(int timeout) {
        // when set if already running, read already blocks and the new value will not become effective now
        if (mReadTimeout == 0 && timeout != 0 && getSensorStatus() != UsbSerialStatus.SERIAL_RUNNING) {
            Log.e(TAG, "Set readTimeout before UsbSerialIOManager is started");
        }
        mReadTimeout = timeout;
    }

    public int getReadTimeout() {
        return mReadTimeout;
    }

    public void setWriteTimeout(int timeout) {
        mWriteTimeout = timeout;
    }

    public int getWriteTimeout() {
        return mWriteTimeout;
    }

    /*
     * when writeAsync is used, it is recommended to use readTimeout != 0,
     * else the write will be delayed until read data is available
     */
    public void writeAsync(byte[] data) {
        synchronized (mWriteBuffer) {
            mWriteBuffer.put(data);
        }
    }

    public synchronized void stop() {
        if (getSensorStatus() == UsbSerialStatus.SERIAL_RUNNING) {
            setSensorStatus(UsbSerialStatus.SERIAL_CLOSE);
        }
    }

    /**
     * Continuously services the read and write buffers until {@link #stop()} is
     * called, or until a driver exception is raised.
     * <p>
     * NOTE(mikey): Uses inefficient read/write-with-timeout.
     */
    @Override
    public void run() {

        synchronized (this) {
            if (getSensorStatus() == UsbSerialStatus.SERIAL_RUNNING) {
                return;
            }
            setSensorStatus(UsbSerialStatus.SERIAL_RUNNING);
        }

        while (getSensorStatus() == UsbSerialStatus.SERIAL_RUNNING) {
            try {
                step();
            } catch (Exception e) {
                final UsbSerialIOListener listener = getSerialIOListener();
                if (listener != null) {
                    listener.onRunError(e);
                }

                String exception = e.getMessage();
                if (exception.contains("Connection closed")) {
                    setSensorStatus(UsbSerialStatus.SERIAL_DISCONNECT);
                } else if (exception.contains("Queueing USB request failed")) {
                    setSensorStatus(UsbSerialStatus.SERIAL_DISCONNECT);
                } else if (exception.contains("Waiting for USB request failed")) {
                    setSensorStatus(UsbSerialStatus.SERIAL_DISCONNECT);
                } else {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void step() throws IOException {
        // Handle incoming data.
        int len = mSerialPort.read(mReadBuffer.array(), mReadTimeout);
        if (len > 0) {
            //Log.v(TAG, "Read data len=" + len);
            final UsbSerialIOListener listener = getSerialIOListener();
            if (listener != null) {
                final byte[] data = new byte[len];
                mReadBuffer.get(data, 0, len);
                listener.onNewData(data);
            }
            mReadBuffer.clear();
        }

        // Handle outgoing data.
        byte[] outBuff = null;
        synchronized (mWriteBuffer) {
            len = mWriteBuffer.position();
            if (len > 0) {
                outBuff = new byte[len];
                mWriteBuffer.rewind();
                mWriteBuffer.get(outBuff, 0, len);
                mWriteBuffer.clear();
            }
        }
        if (outBuff != null) {
            //Log.v(TAG, "Writing data len=" + len);
            mSerialPort.write(outBuff, mWriteTimeout);
        }
    }
}
